所谓 热替换,指的是在不需要重启应用的前提下修复类的已知问题。
如果一个类已经被虚拟机所加载,要修正该类的某些方法,只能通过实现 热替换 来实现:”在 navite 层替换到对应被虚拟机加载过的类的方法”。在业界还有其他冷启动的方案,比如 tinker,让 Classloader 去加载新的类,而原来的类还在虚拟机中,不重启应用是无法加载新的类的。
以阿里 Andfix 开源项目 及 Sophix 方案为分析。
- AndFix#replaceMethod(Method src,Method dest) 为 Java 层替换错误方法的入口,通过 JNI 调用 Navite 层代码
andifx#replaceMethod 为 Navite 层被上层所调用的代码,对虚拟机内的方法进行 ”替换“
1
2
3
4
5
6
7static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}代码区分了 Dalvik 虚拟机和 Art 虚拟机的不同实现
art_method_replace#art_replaceMethod 实现 Art 虚拟机上的替换
1
2
3
4
5
6
7
8
9
10
11
12
13extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 23) {
replace_7_0(env, src, dest);
} else if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else if (apilevel > 19) {
replace_5_0(env, src, dest);
}else{
replace_4_4(env, src, dest);
}
}
不同的虚拟机版本,由于虚拟机底层数据结构并不相同,所以还进一步针对不同 Android 版本再做区分
* **[art_method_replace_4_4#replace_4_4](https://github.com/alibaba/AndFix/blob/master/jni/art/art_method_replace_4_4.cpp)**
* **[art_method_replace_5_0#replace_5_0](https://github.com/alibaba/AndFix/blob/master/jni/art/art_method_replace_5_0.cpp)**
* **[art_method_replace_5_1#replace_5_1](https://github.com/alibaba/AndFix/blob/master/jni/art/art_method_replace_5_1.cpp)**
* **[art_method_replace_6_0#replace_6_0](https://github.com/alibaba/AndFix/blob/master/jni/art/art_method_replace_6_0.cpp)**
* **[art_method_replace_7_0#replace_7_0](https://github.com/alibaba/AndFix/blob/master/jni/art/art_method_replace_7_0.cpp)**
以 6.0 版本的 Art虚拟机替换为例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
//获取 被替换 Method 对象对应 ArtMethod 的地址
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
//获取 替换 Method 对象对应 ArtMethod 的地址
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
//确保 Classloader 一致
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
// 旧的函数所有成员变量都需要替换成新的函数的成员变量
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_6_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}每一个 Java 方法在 Art 虚拟机内都对应一个 art_method, 用于记录 Java 方法的所有信息,包括归属类,访问权限,代码执行地址等等。替换完之后,再次调用替换方法的时候就会直接运行到新方法的实现。
Java Code 会被编译成 Dex Code ,然后被 Art 虚拟机加载,可通过解释模式或者 AOT模式执行.但都先需要获取方法的执行入口:
- 解释模式,获取 art_method.entry_point_from_jni_
AOT模式模式,获取 art_method.entry_point_from_jni_
要实现方法替换,除了替换这几个指针入口地址.
上述的例子需要保证 art_method_replace_6_0#replace_6_0 所用的数据结构与 art_method 对应的数据结构完全一致才可以.但是由于各种厂商存在各式各样经过改造的 ROM,难以保证能够修复成功.
Sophix 探索出了一种突破底层结构差异的方法。这种方法把一个 art_method 看成了一个整体进行替换而不必针对每个版本的 method 严格控制内容。换句话说,只要知道当前设备 art_method 的长度,就可以把整个结构体完全替换掉。
由于 ArtMethod 是紧密排列的,所以相邻两个 ArtMethod 的起始地址差值就是 ArtMethod 的大小。通过定义一个简单的类来计算。
1
2
3
4public class NativeMethodCal{
final public static void f1(){}
final public static void f2(){}
}两个方法属于 static 方法 且该类只有这两个方法,所以必定相邻,计算如下
1
2
3size_t firMid = (size_t) env->GetStaticMethodID(nativeMethodCalClazz,"f1","()V");
size_t secMid = (size_t) env->GetStaticMethodID(nativeMethodCalClazz,"f2","()V");
size_t methodSize = secMid - firMidnavice 层的替换可为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void replacee(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
//确保 Classloader 一致
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
size_t firMid = (size_t) env->GetStaticMethodID(nativeMethodCalClazz,"f1","()V");
size_t secMid = (size_t) env->GetStaticMethodID(nativeMethodCalClazz,"f2","()V");
size_t methodSize = secMid - firMid
memcpy(smeth,dmeth, methodSize);
}
存在的问题与限制
- 针对反射调用非静态方法产生的问题。这类问题只能通过冷启动修复,原因是反射调用的
invoke
底层回调用到InvokeMethod
,该方法会校验反射的对象和是不是 ArtMethod 的一个实例。由于替换了 ArtMethod 导致匹配不上。 - 不适合类发生结构变化的修改。比如增删方法可能引起类及 Dex 方法数变化,进而改变方法索引。同样地,增删字段也会更改方法索引。
参考资料